feat(guest): turn-based guest participation (fix iOS guest never replying)#3
Merged
Conversation
New node-safe module shared by the renderer and the Mac bridge: the steering preamble, parent-transcript peer-context builder, the full guest prompt composer, and the guest->parent reply mirror message builder (deduped by guestRunId). Provider-label is injected so the module stays free of any renderer/main label-resolution split. Foundation for making guest participation turn-based AND reaching iOS-origin turns (which run through the bridge, not the renderer). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…turns) iOS-origin turns run through the bridge composerPromptFn, which only ever dispatched the host — so a configured guest never replied on the phone (the trigger lived only in the renderer). Now the bridge: - tells the host a guest is attached (passes guestParticipant to composeRunPrompt) so it can anticipate/avoid conflicts; - routes @-tags like ensemble (@guest -> guest only, @parent/@host -> host only, no tag -> host then guest); - after the host run finalizes (reply persisted), dispatches the guest as a normal leaf run on its child chat so it answers WITH the host's reply in context (turn-based); - mirrors the guest's reply into the parent transcript on the guest run's finalize (guest-return-${runId}, deduped by guestRunId). Driven via a module-ref runner assigned in the bridge closure so the module-scope finalizeBridgeRunTranscript can reach the closure dispatch helpers. iOS needs no changes — it already renders guestParticipantReply. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The renderer fanned the guest out in parallel with the host, so the guest never saw the host's reply to the same turn. Now a fan-out send defers the guest: it's dispatched from the run-completion handler once the host run on that chat finishes, with the host's reply already in the parent transcript (passed via a fresh chatRecord). @guest stays immediate (guest only); @parent/@host stays host only. This matches the new bridge behavior so desktop and iOS guest chats now behave identically (turn-based). Also drop the host-success gate on the bridge guest dispatch so the guest still answers when the host run fails (fallback / second opinion), matching the renderer completion hook which fires regardless of exit code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
boggspa
added a commit
that referenced
this pull request
Jun 18, 2026
Build 21 ships the merged iOS work: turn-based guest participation (PR #3, Mac-side bridge), active-chat/sidebar/Settings-sheet state preservation across settings changes (PR #4), and surfaced new global/ensemble chat create failures with retry instead of an infinite spinner (PR #5). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Guest chats (host + one attached guest) worked on desktop but the guest never replied on iOS — you could add/configure a guest, but only starting a side-chat with it directly produced a response.
Root cause (GAP B from the 2026-06-16 guest-parity review): the guest trigger + reply-mirror lived only in the renderer (
App.tsx). iOS-origin turns run through the Mac bridge'scomposerPromptFn, which never touches the renderer — so it only ever dispatched the host. The bridge didn't even tell the host a guest was attached.Separately, desktop was a parallel fan-out (host and guest fired together), so the guest never saw the host's reply to the same turn.
Fix — turn-based guest participation on both platforms
Per the chosen behaviour: host responds first, then the guest (so the guest sees the host's reply); an
@-tag aims the prompt at only that agent (ensemble-style).src/main/GuestParticipantRun.ts(node-safe, unit-tested): steering preamble, parent-transcript peer-context builder, full guest-prompt composer, and theguest-return-${runId}reply-mirror message builder (deduped byguestRunId). Provider-label injected so it's tree-agnostic.composerPromptFnnow passesguestParticipantto the host prompt (host-awareness), routes@-tags (@guest→ guest only,@parent/@host→ host only, no tag → host then guest), and after the host run finalizes dispatches the guest as a normal leaf run on its child chat (so it answers with the host's reply in context), then mirrors the guest's reply into the parent transcript on the guest run's finalize. Wired via a module-ref runner assigned in the bridge closure so the module-scopefinalizeBridgeRunTranscriptcan reach the closure dispatch helpers.chatRecord), instead of firing in parallel.@gueststays immediate;@parent/@hoststays host-only.iOS needs no changes — it already renders
guestParticipantReplyprovider-tinted/attributed (GAP A,cb74513a) and excludes the guest from the side-chats tab (GAP C,56899180). Once the Mac mirrors + broadcasts the guest reply, the phone shows it.Verification
npm run typecheck:node+npm run build(main/preload/renderer) — green; confirms the cross-treeextractGuestParticipantAddressTargetimport bundles into main.GuestParticipantRun.test.ts(13).@-routing already covered byComposerMentionTrigger.test.ts; host guest-context byPromptComposition.test.ts.Remaining verification (not in this PR)
@guest/@parentroute correctly.🤖 Generated with Claude Code